using System.Data;
using System.Security.Cryptography.X509Certificates;
using System.Diagnostics;
using System.Collections.Generic;
using System;
using UnityEngine;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Debug = UnityEngine.Debug;
using System.IO;
using System.Runtime.InteropServices;
using Reinterpret.Net;
using System.Runtime.CompilerServices;
using EarthGeospatial;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Geometry;
using EarthUtility;
using Unity.Mathematics;
using System.Linq;
using Cysharp.Threading.Tasks;

namespace Cesium3DTilesSelection
{
    internal class QuantizedMeshContent : TileContentLoader
    {
        public static string CONTENT_TYPE = "application/vnd.quantized-mesh";

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        struct QuantizedMeshHeader
        {
            // The center of the tile in Earth-centered Fixed coordinates.
            public double CenterX;
            public double CenterY;
            public double CenterZ;

            // The minimum and maximum heights in the area covered by this tile.
            // The minimum may be lower and the maximum may be higher than
            // the height of any vertex in this tile in the case that the min/max vertex
            // was removed during mesh simplification, but these are the appropriate
            // values to use for analysis or visualization.
            public float MinimumHeight;
            public float MaximumHeight;

            // The tile’s bounding sphere.  The X,Y,Z coordinates are again expressed
            // in Earth-centered Fixed coordinates, and the radius is in meters.
            public double BoundingSphereCenterX;
            public double BoundingSphereCenterY;
            public double BoundingSphereCenterZ;
            public double BoundingSphereRadius;

            // The horizon occlusion point, expressed in the ellipsoid-scaled
            // Earth-centered Fixed frame. If this point is below the horizon, the entire
            // tile is below the horizon. See
            // http://cesiumjs.org/2013/04/25/Horizon-culling/ for more information.
            public double HorizonOcclusionPointX;
            public double HorizonOcclusionPointY;
            public double HorizonOcclusionPointZ;

            // The total number of vertices.
            public int vertexCount;
        };
        struct ExtensionHeader
        {

            byte extensionId;
            UInt32 extensionLength;
        };
        enum QuantizedMeshIndexType
        {
            UnsignedShort,
            UnsignedInt
        };
        private const int headerLength = 92;
        private const int extensionHeaderLength = 5;
        public override UniTask<TileContentLoadResult> load(TileContentLoadInput input)
        {
            byte[] data = input.pRequest.downloadHandler.data;
            string url = input.pRequest.url;
            return load(input.tileID, input.tileBoundingVolume, url, data, input.contentOptions.enableWaterMask);
            // return UniTask.Run( ()=>load(input.tileID, input.tileBoundingVolume, url,data , input.contentOptions.enableWaterMask));
        }


        struct QuantizedMeshView
        {
            // public QuantizedMeshView()
            // {
            //     header = 0;
            //     indexType = QuantizedMeshIndexType.UnsignedShort;
            //     triangleCount = 0;
            //     westEdgeIndicesCount = 0;
            //     southEdgeIndicesCount = 0;
            //     eastEdgeIndicesCount = 0;
            //     northEdgeIndicesCount = 0;
            //     onlyWater = false;
            //     onlyLand = true;
            //     metadataJsonLength = 0;
            // }

            public QuantizedMeshHeader header;
            public UInt16[] uBuffer;
            public UInt16[] vBuffer;
            public UInt16[] heightBuffer;

            public QuantizedMeshIndexType indexType;
            public UInt32 triangleCount;
            public byte[] indicesBuffer;
            public UInt32 westEdgeIndicesCount;
            public byte[] westEdgeIndicesBuffer;

            public UInt32 southEdgeIndicesCount;
            public byte[] southEdgeIndicesBuffer;

            public UInt32 eastEdgeIndicesCount;
            public byte[] eastEdgeIndicesBuffer;

            public UInt32 northEdgeIndicesCount;
            public byte[] northEdgeIndicesBuffer;

            public byte[] octEncodedNormalBuffer;

            public bool onlyWater;
            public bool onlyLand;

            // water mask will always be a 256*256 map where 0 is land and 255 is water.
            public byte[] waterMaskBuffer;

            public UInt32 metadataJsonLength;
            public string metadataJsonBuffer;
        };
        private static T FromBinaryReader<T>(BinaryReader reader) where T : struct
        {

            // Read in a byte array
            byte[] bytes = reader.ReadBytes(Marshal.SizeOf(typeof(T)));

            // Pin the managed memory while, copy it out the data, then unpin it
            GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
            T theStructure = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
            handle.Free();

            return theStructure;
        }
        int zigZagDecode(int value)
        {
            return (value >> 1) ^ (-(value & 1));
        }

        private async UniTask<TileContentLoadResult> load(TileID tileid, BoundingVolume tileBoundingVolume, string url, byte[] data, bool enableWaterMask)
        {
            using (MemoryStream mem = new MemoryStream(data))
            {
                using (BinaryReader reader = new BinaryReader(mem))
                {
                    if (reader.BaseStream.Length > headerLength)
                    {
                        QuadtreeTileID id = tileid.quadtreeID.Value;
                        TileContentLoadResult pResult = new TileContentLoadResult();
                        QuantizedMeshView? meshView = parseQuantizedMesh(reader, enableWaterMask);
                        if (!meshView.HasValue)
                        {
                            Debug.Log(string.Format("Unable to parse quantized-mesh-1.0 tile {0}.", url));
                            return pResult;
                        }

                        BoundingRegion pRegion = tileBoundingVolume.pBoundingRegion;
                        if (!pRegion)
                        {
                            BoundingRegionWithLooseFittingHeights pLooseRegion = tileBoundingVolume.pLooseRegion;
                            if (pLooseRegion)
                            {
                                pRegion = pLooseRegion.getBoundingRegion();
                            }
                        }
                        if (!pRegion)
                        {
                            Debug.Log(string.Format("Unable to create quantized-mesh-1.0 tile {0} because the tile's bounding volume is not a bounding region.", url));
                            return new TileContentLoadResult();
                        }
                        // get vertex count for this mesh
                        QuantizedMeshHeader pHeader = meshView.Value.header;
                        int vertexCount = pHeader.vertexCount;
                        uint indicesCount = meshView.Value.triangleCount * 3;
                        uint skirtVertexCount = meshView.Value.westEdgeIndicesCount + meshView.Value.southEdgeIndicesCount + meshView.Value.eastEdgeIndicesCount + meshView.Value.northEdgeIndicesCount;
                        uint skirtIndicesCount = (skirtVertexCount - 4) * 6;

                        // decode position without skirt, but preallocate position buffer to include
                        // skirt as well

                        float[] outputPositions = new float[(vertexCount + skirtVertexCount) * 3];
                        int positionOutputIndex = 0;
                        var center = new double3(pHeader.BoundingSphereCenterX, pHeader.BoundingSphereCenterY, pHeader.BoundingSphereCenterZ);
                        var horizonOcclusionPoint = new double3(pHeader.HorizonOcclusionPointX, pHeader.HorizonOcclusionPointY, pHeader.HorizonOcclusionPointZ);
                        double minimumHeight = pHeader.MinimumHeight;
                        double maximumHeight = pHeader.MaximumHeight;

                        double minX = double.MaxValue;
                        double minY = double.MaxValue;
                        double minZ = double.MaxValue;
                        double maxX = double.MinValue;
                        double maxY = double.MinValue;
                        double maxZ = double.MinValue;

                        Ellipsoid ellipsoid = Ellipsoid.WGS84;
                        var rectangle = pRegion.getRectangle();
                        double west = rectangle.getWest();
                        double south = rectangle.getSouth();
                        double east = rectangle.getEast();
                        double north = rectangle.getNorth();
                        int u = 0;
                        int v = 0;
                        int height = 0;

                        NativeArray<double3> uvsAndHeights = new NativeArray<double3>(vertexCount, Allocator.Temp);
                        for (int i = 0; i < vertexCount; i++)
                        {
                            u += zigZagDecode(meshView.Value.uBuffer[i]);
                            v += zigZagDecode(meshView.Value.vBuffer[i]);
                            height += zigZagDecode(meshView.Value.heightBuffer[i]);

                            double uRatio = u / 32767.0;
                            double vRatio = v / 32767.0;
                            double heightRatio = height / 32767.0;

                            double longitude = math.lerp(west, east, uRatio);
                            double latitude = math.lerp(south, north, vRatio);
                            double heightMeters = math.lerp(minimumHeight, maximumHeight, heightRatio);

                            double3 position = ellipsoid.cartographicToCartesian(new Cartographic(longitude, latitude, heightMeters));
                            position -= center;
                            outputPositions[positionOutputIndex++] = (float)position.x;
                            outputPositions[positionOutputIndex++] = (float)position.y;
                            outputPositions[positionOutputIndex++] = (float)position.z;

                            minX = math.min(minX, position.x);
                            minY = math.min(minY, position.y);
                            minZ = math.min(minZ, position.z);

                            maxX = math.max(maxX, position.x);
                            maxY = math.max(maxY, position.y);
                            maxZ = math.max(maxZ, position.z);

                            uvsAndHeights[i] = new double3(uRatio, vRatio, heightRatio);
                        }

                        // decode normal vertices of the tile as well as its metadata without skirt
                        byte[] outputNormalsBuffer = null;
                        float[] outputNormals = null;
                        if (meshView.Value.octEncodedNormalBuffer != null)
                        {
                            int totalNormalFloats = (vertexCount + (int)skirtVertexCount) * 3;
                            outputNormalsBuffer = new byte[(totalNormalFloats * sizeof(float))];
                            outputNormals = new float[totalNormalFloats];
                            decodeNormals(meshView.Value.octEncodedNormalBuffer, outputNormals);
                            // outputNormals = gsl::span<float>(reinterpret_cast<float*>(outputNormalsBuffer.data()),outputNormalsBuffer.size() / sizeof(float));
                        }

                        // decode metadata
                        if (meshView.Value.metadataJsonLength > 0)
                        {
                            processMetadata(id, meshView.Value.metadataJsonBuffer, pResult);
                        }
                        // indices buffer for gltf to include tile and skirt indices. Caution of
                        // indices type since adding skirt means the number of vertices is potentially
                        // over maximum of uint16_t
                        byte[] outputIndicesBuffer;
                        int indexSizeBytes = meshView.Value.indexType == QuantizedMeshIndexType.UnsignedInt ? Unsafe.SizeOf<uint>() : Unsafe.SizeOf<UInt16>();
                        double skirtHeight = calculateSkirtHeight(ellipsoid, rectangle);
                        double longitudeOffset = (east - west) * 0.0001;
                        double latitudeOffset = (north - south) * 0.0001;
                        if (meshView.Value.indexType == QuantizedMeshIndexType.UnsignedInt)
                        {
                            // decode the tile indices without skirt.
                            uint outputIndicesCount = indicesCount + skirtIndicesCount;
                            outputIndicesBuffer = new byte[outputIndicesCount * Unsafe.SizeOf<UInt32>()];
                            var indices = meshView.Value.indicesBuffer.ReinterpretToArray<uint>();

                            var outputIndices = new UInt32[outputIndicesCount];
                            decodeIndices(indices, outputIndices);
                            outputIndicesBuffer = outputIndices.Reinterpret();
                            // generate normals if no provided
                            if (outputNormalsBuffer != null)
                            {
                                outputNormals = outputNormalsBuffer.ReinterpretToArray<float>();
                                outputNormalsBuffer = generateNormals(outputPositions, outputIndices, indicesCount);
                            }
                            // add skirt
                            addSkirts<UInt32, UInt32>(
                                ellipsoid,
                                center,
                                rectangle,
                                minimumHeight,
                                maximumHeight,
                                vertexCount,
                                (int)indicesCount,
                                skirtHeight,
                                longitudeOffset,
                                latitudeOffset,
                                uvsAndHeights,
                                meshView.Value.westEdgeIndicesBuffer,
                                meshView.Value.southEdgeIndicesBuffer,
                                meshView.Value.eastEdgeIndicesBuffer,
                                meshView.Value.northEdgeIndicesBuffer,
                                outputPositions,
                                outputNormals,
                                outputIndices);

                            indexSizeBytes = Unsafe.SizeOf<UInt32>();

                        }

                        else
                        {
                            uint outputIndicesCount = indicesCount + skirtIndicesCount;
                            var indices = meshView.Value.indicesBuffer.ReinterpretToArray<UInt16>();
                            if (vertexCount + skirtVertexCount < UInt16.MaxValue)
                            {
                                // decode the tile indices without skirt.
                                outputIndicesBuffer = new byte[outputIndicesCount * Unsafe.SizeOf<UInt16>()];
                                // outputIndicesBuffer.resize(outputIndicesCount * Unsafe.SizeOf<UInt16>());
                                var outputIndices = new UInt16[outputIndicesCount];//   outputIndicesBuffer.ReinterpretToArray<UInt16>();
                                decodeIndices(indices, outputIndices);
                                outputIndicesBuffer = outputIndices.Reinterpret();

                                // generate normals if no provided
                                if (outputNormalsBuffer != null)
                                {
                                    outputNormalsBuffer = generateNormals(outputPositions, outputIndices, indicesCount);
                                    outputNormals = outputNormalsBuffer.ReinterpretToArray<float>();
                                }

                                addSkirts<UInt16, UInt16>(
                                    ellipsoid,
                                    center,
                                    rectangle,
                                    minimumHeight,
                                    maximumHeight,
                                    vertexCount,
                                    (int)indicesCount,
                                    skirtHeight,
                                    longitudeOffset,
                                    latitudeOffset,
                                    uvsAndHeights,
                                    meshView.Value.westEdgeIndicesBuffer,
                                    meshView.Value.southEdgeIndicesBuffer,
                                    meshView.Value.eastEdgeIndicesBuffer,
                                    meshView.Value.northEdgeIndicesBuffer,
                                    outputPositions,
                                    outputNormals,
                                    outputIndices);

                                indexSizeBytes = Unsafe.SizeOf<UInt16>();
                            }
                            else
                            {
                                outputIndicesBuffer = new byte[outputIndicesCount * sizeof(UInt32)];
                                var outputIndices = outputIndicesBuffer.ReinterpretToArray<UInt32>();
                                decodeIndices(indices, outputIndices);

                                // generate normals if no provided
                                if (outputNormalsBuffer != null)
                                {
                                    outputNormalsBuffer = generateNormals(outputPositions, outputIndices, indicesCount);
                                    outputNormals = outputNormalsBuffer.ReinterpretToArray<float>();
                                }
                                addSkirts<UInt16, UInt32>(
                                    ellipsoid,
                                    center,
                                    rectangle,
                                    minimumHeight,
                                    maximumHeight,
                                    vertexCount,
                                    (int)indicesCount,
                                    skirtHeight,
                                    longitudeOffset,
                                    latitudeOffset,
                                    uvsAndHeights,
                                    meshView.Value.westEdgeIndicesBuffer,
                                    meshView.Value.southEdgeIndicesBuffer,
                                    meshView.Value.eastEdgeIndicesBuffer,
                                    meshView.Value.northEdgeIndicesBuffer,
                                    outputPositions,
                                    outputNormals,
                                    outputIndices);

                                indexSizeBytes = Unsafe.SizeOf<UInt32>();
                            }
                        }

                        pResult.model = new Gltf.Model();
                        Gltf.Model model = pResult.model;

                        Gltf.Material material = new Gltf.Material();
                        model.materials.Add(material);

                        material.pbrMetallicRoughness = new Gltf.MaterialPBRMetallicRoughness();
                        Gltf.MaterialPBRMetallicRoughness pbr = material.pbrMetallicRoughness;
                        pbr.metallicFactor = 0.0;
                        pbr.roughnessFactor = 1.0;
                        int meshId = model.meshes.Count;
                        model.meshes.Add(new Gltf.Mesh());
                        Gltf.Mesh mesh = model.meshes[meshId];
                        mesh.primitives.Add(new Gltf.MeshPrimitive());

                        Gltf.MeshPrimitive primitive = mesh.primitives[0];
                        primitive.mode = Gltf.MeshPrimitive.Mode.TRIANGLES;
                        primitive.material = 0;

                        // add position buffer to gltf
                        int positionBufferId = model.buffers.Count;
                        model.buffers.Add(new Gltf.Buffer());
                        Gltf.Buffer positionBuffer = model.buffers[positionBufferId];
                        positionBuffer.cesium.data = outputPositions.Reinterpret();

                        int positionBufferViewId = model.bufferViews.Count;
                        model.bufferViews.Add(new Gltf.BufferView());
                        Gltf.BufferView positionBufferView = model.bufferViews[positionBufferViewId];
                        positionBufferView.buffer = (int)(positionBufferId);
                        positionBufferView.byteOffset = 0;
                        positionBufferView.byteStride = 3 * sizeof(float);
                        positionBufferView.byteLength = (Int64)(positionBuffer.cesium.data.Length);
                        positionBufferView.target = Gltf.BufferView.Target.ARRAY_BUFFER;

                        int positionAccessorId = model.accessors.Count;
                        model.accessors.Add(new Gltf.Accessor());
                        Gltf.Accessor positionAccessor = model.accessors[positionAccessorId];
                        positionAccessor.bufferView = (int)(positionBufferViewId);
                        positionAccessor.byteOffset = 0;
                        positionAccessor.componentType = Gltf.Accessor.ComponentType.FLOAT;
                        positionAccessor.count = vertexCount + skirtVertexCount;
                        positionAccessor.type = Gltf.Accessor.Type.VEC3;
                        positionAccessor.min = new List<double> { minX, minY, minZ };
                        positionAccessor.max = new List<double> { maxX, maxY, maxZ };

                        primitive.attributes["POSITION"] = (int)(positionAccessorId);

                        // add normal buffer to gltf if there are any
                        if (outputNormalsBuffer != null)
                        {
                            int normalBufferId = model.buffers.Count;
                            model.buffers.Add(new Gltf.Buffer());
                            Gltf.Buffer normalBuffer = model.buffers[normalBufferId];
                            normalBuffer.cesium.data = outputNormalsBuffer;

                            int normalBufferViewId = model.bufferViews.Count;
                            model.bufferViews.Add(new Gltf.BufferView());
                            Gltf.BufferView normalBufferView = model.bufferViews[normalBufferViewId];
                            normalBufferView.buffer = (int)(normalBufferId);
                            normalBufferView.byteOffset = 0;
                            normalBufferView.byteStride = 3 * sizeof(float);
                            normalBufferView.byteLength = (Int64)(normalBuffer.cesium.data.Length);
                            normalBufferView.target = Gltf.BufferView.Target.ARRAY_BUFFER;

                            int normalAccessorId = model.accessors.Count;
                            model.accessors.Add(new Gltf.Accessor());
                            Gltf.Accessor normalAccessor = model.accessors[normalAccessorId];
                            normalAccessor.bufferView = (int)(normalBufferViewId);
                            normalAccessor.byteOffset = 0;
                            normalAccessor.componentType = Gltf.Accessor.ComponentType.FLOAT;
                            normalAccessor.count = vertexCount + skirtVertexCount;
                            normalAccessor.type = Gltf.Accessor.Type.VEC3;

                            primitive.attributes["NORMAL"] = (int)(normalAccessorId);
                        }

                        // add indices buffer to gltf
                        int indicesBufferId = model.buffers.Count;
                        model.buffers.Add(new Gltf.Buffer());
                        Gltf.Buffer indicesBuffer = model.buffers[indicesBufferId];
                        indicesBuffer.cesium.data = outputIndicesBuffer;

                        int indicesBufferViewId = model.bufferViews.Count;
                        model.bufferViews.Add(new Gltf.BufferView());
                        Gltf.BufferView indicesBufferView = model.bufferViews[indicesBufferViewId];
                        indicesBufferView.buffer = (int)(indicesBufferId);
                        indicesBufferView.byteOffset = 0;
                        indicesBufferView.byteLength = (Int64)(indicesBuffer.cesium.data.Length);
                        indicesBufferView.byteStride = indexSizeBytes;
                        indicesBufferView.target = Gltf.BufferView.Target.ARRAY_BUFFER;

                        int indicesAccessorId = model.accessors.Count;
                        model.accessors.Add(new Gltf.Accessor());
                        Gltf.Accessor indicesAccessor = model.accessors[indicesAccessorId];
                        indicesAccessor.bufferView = (int)(indicesBufferViewId);
                        indicesAccessor.byteOffset = 0;
                        indicesAccessor.type = Gltf.Accessor.Type.SCALAR;
                        indicesAccessor.count = indicesCount + skirtIndicesCount;
                        indicesAccessor.componentType = indexSizeBytes == sizeof(UInt32)
                                ? Gltf.Accessor.ComponentType.UNSIGNED_INT
                                : Gltf.Accessor.ComponentType.UNSIGNED_SHORT;

                        primitive.indices = (int)(indicesBufferId);

                        // add skirts info to primitive extra in case we need to upsample from it
                        SkirtMeshMetadata skirtMeshMetadata = new SkirtMeshMetadata();
                        skirtMeshMetadata.noSkirtIndicesBegin = 0;
                        skirtMeshMetadata.noSkirtIndicesCount = (int)indicesCount;
                        skirtMeshMetadata.meshCenter = center;
                        skirtMeshMetadata.skirtWestHeight = skirtHeight;
                        skirtMeshMetadata.skirtSouthHeight = skirtHeight;
                        skirtMeshMetadata.skirtEastHeight = skirtHeight;
                        skirtMeshMetadata.skirtNorthHeight = skirtHeight;

                        primitive.extras = SkirtMeshMetadata.createGltfExtras(skirtMeshMetadata);

                        // add only-water and only-land flags to primitive extras
                        primitive.extras["OnlyWater"] = meshView.Value.onlyWater;
                        primitive.extras["OnlyLand"] = meshView.Value.onlyLand;

                        // TODO: use KHR_texture_transform
                        primitive.extras["WaterMaskTranslationX"] = 0.0;
                        primitive.extras["WaterMaskTranslationY"] = 0.0;
                        primitive.extras["WaterMaskScale"] = 1.0;

                        // if there is a combination of water and land, add the full water mask
                        if (!meshView.Value.onlyWater && !meshView.Value.onlyLand)
                        {

                            // create source image
                            int waterMaskImageId = model.images.Count;
                            model.images.Add(new Gltf.Image());
                            Gltf.Image waterMaskImage = model.images[waterMaskImageId];
                            waterMaskImage.cesium.width = 256;
                            waterMaskImage.cesium.height = 256;
                            waterMaskImage.cesium.channels = 1;
                            waterMaskImage.cesium.bytesPerChannel = 1;
                            waterMaskImage.cesium.pixelData = new byte[65536];
                            Array.Copy(meshView.Value.waterMaskBuffer, waterMaskImage.cesium.pixelData, 65536);

                            // create sampler parameters
                            int waterMaskSamplerId = model.samplers.Count;
                            model.samplers.Add(new Gltf.Sampler());
                            Gltf.Sampler waterMaskSampler = model.samplers[waterMaskSamplerId];
                            waterMaskSampler.magFilter = Gltf.Sampler.MagFilter.LINEAR;
                            waterMaskSampler.minFilter = Gltf.Sampler.MinFilter.LINEAR_MIPMAP_NEAREST;
                            waterMaskSampler.wrapS = Gltf.Sampler.WrapS.CLAMP_TO_EDGE;
                            waterMaskSampler.wrapT = Gltf.Sampler.WrapT.CLAMP_TO_EDGE;

                            // create texture
                            int waterMaskTextureId = model.textures.Count;
                            model.textures.Add(new Gltf.Texture());
                            Gltf.Texture waterMaskTexture = model.textures[waterMaskTextureId];
                            waterMaskTexture.sampler = (int)(waterMaskSamplerId);
                            waterMaskTexture.source = (int)(waterMaskImageId);

                            // store the texture id in the extras
                            primitive.extras["WaterMaskTex"] = (int)(waterMaskTextureId);
                        }
                        else
                        {
                            primitive.extras["WaterMaskTex"] = (int)(-1);
                        }

                        // create node and update bounding volume
                        model.nodes = new List<Gltf.Node>();
                        model.nodes.Add(new Gltf.Node());
                        Gltf.Node node = model.nodes[0];
                        node.mesh = 0;
                        node.matrix = math.transpose(new double4x4(1.0, 0.0, 0.0, 0.0,
                                                    0.0, 0.0, -1.0, 0.0,
                                                    0.0, 1.0, 0.0, 0.0,
                                                    center.x, center.z, -center.y, 1.0));
                        pResult.updatedBoundingVolume = new BoundingVolume(new BoundingRegion(rectangle, minimumHeight, maximumHeight));

                        if (pResult.model)
                        {
                            pResult.model.extras["Cesium3DTiles_TileUrl"] = url;
                        }
                        return pResult;
                    }
                }
            }
            return new TileContentLoadResult();
        }

        private void decodeNormals(byte[] encoded, float[] decoded)
        {
            if (decoded.Length < encoded.Length)
            {
                throw new System.Exception("decoded buffer is too small.");
            }

            int normalOutputIndex = 0;
            for (int i = 0; i < encoded.Length; i += 2)
            {
                double3 normal = octDecode(encoded[i], encoded[i + 1]);
                decoded[normalOutputIndex++] = (float)(normal.x);
                decoded[normalOutputIndex++] = (float)(normal.y);
                decoded[normalOutputIndex++] = (float)(normal.z);
            }
        }

        private double3 octDecode(byte x, byte y)
        {
            const int rangeMax = 255;

            double3 result;

            result.x = Mathd.fromSNorm(x, rangeMax);
            result.y = Mathd.fromSNorm(y, rangeMax);
            result.z = 1.0 - (math.abs(result.x) + math.abs(result.y));

            if (result.z < 0.0)
            {
                double oldVX = result.x;
                result.x = (1.0 - math.abs(result.y)) * Mathd.signNotZero(oldVX);
                result.y = (1.0 - math.abs(oldVX)) * Mathd.signNotZero(result.y);
            }

            return math.normalize(result);
        }

        private unsafe QuantizedMeshView? parseQuantizedMesh(BinaryReader reader, bool enableWaterMask)
        {
            QuantizedMeshView meshView = new QuantizedMeshView();
            meshView.onlyLand = true;
            meshView.header = FromBinaryReader<QuantizedMeshHeader>(reader);
            var vertexCount = meshView.header.vertexCount;
            var sizeofUint32 = Unsafe.SizeOf<UInt32>();
            var ubuffer = reader.ReadBytes(vertexCount * UnsafeUtility.SizeOf<UInt16>());
            meshView.uBuffer = ubuffer.ReinterpretToArray<UInt16>();
            if (isEOF(reader)) return null;
            var vbuffer = reader.ReadBytes(vertexCount * UnsafeUtility.SizeOf<UInt16>());
            meshView.vBuffer = vbuffer.ReinterpretToArray<UInt16>();
            if (isEOF(reader)) return null;
            var heightbuffer = reader.ReadBytes(vertexCount * UnsafeUtility.SizeOf<UInt16>());
            meshView.heightBuffer = heightbuffer.ReinterpretToArray<UInt16>();
            if (isEOF(reader)) return null;

            long readIndex = reader.BaseStream.Position;
            UInt32 indexSizeBytes;
            if (vertexCount > 65536)
            {
                // 32-bit indices
                if ((readIndex % 4) != 0)
                {
                    readIndex += 2;
                    if (readIndex > reader.BaseStream.Length)
                    {
                        return null;
                    }
                }
                meshView.triangleCount = reader.ReadUInt32();
                UInt32 indicesCount = meshView.triangleCount * 3;
                var indicesBuffer = reader.ReadBytes((int)indicesCount * UnsafeUtility.SizeOf<UInt16>());
                meshView.heightBuffer = heightbuffer.ReinterpretToArray<UInt16>();
                meshView.indicesBuffer = indicesBuffer;

                meshView.indexType = QuantizedMeshIndexType.UnsignedInt;
                indexSizeBytes = (uint)Unsafe.SizeOf<UInt32>();
            }
            else
            {
                // 16-bit indices
                meshView.triangleCount = reader.ReadUInt32();
                if (isEOF(reader)) return null;


                UInt32 indicesCount = meshView.triangleCount * 3;
                meshView.indicesBuffer = reader.ReadBytes((int)indicesCount * Unsafe.SizeOf<UInt16>());
                if (isEOF(reader)) return null;

                meshView.indexType = QuantizedMeshIndexType.UnsignedShort;
                indexSizeBytes = (uint)Unsafe.SizeOf<UInt16>();
            }
            // prepare to read edge
            uint edgeByteSizes = 0;
            // read the west edge indices
            meshView.westEdgeIndicesCount = reader.ReadUInt32();
            edgeByteSizes = meshView.westEdgeIndicesCount * indexSizeBytes;
            if (isEOF(reader)) return null;
            meshView.westEdgeIndicesBuffer = reader.ReadBytes((int)edgeByteSizes);

            // read the south edge
            meshView.southEdgeIndicesCount = reader.ReadUInt32();
            edgeByteSizes = meshView.southEdgeIndicesCount * indexSizeBytes;
            if (isEOF(reader)) return null;
            meshView.southEdgeIndicesBuffer = reader.ReadBytes((int)edgeByteSizes);

            // read the east edge
            meshView.eastEdgeIndicesCount = reader.ReadUInt32();
            edgeByteSizes = meshView.eastEdgeIndicesCount * indexSizeBytes;
            if (isEOF(reader)) return null;
            meshView.eastEdgeIndicesBuffer = reader.ReadBytes((int)edgeByteSizes);

            // read the north edge
            meshView.northEdgeIndicesCount = reader.ReadUInt32();
            edgeByteSizes = meshView.northEdgeIndicesCount * indexSizeBytes;
            if (isEOF(reader)) return null;
            meshView.northEdgeIndicesBuffer = reader.ReadBytes((int)edgeByteSizes);

            // parse oct-encoded normal buffer and metadata
            while (reader.BaseStream.Position < reader.BaseStream.Length)
            {
                if (reader.BaseStream.Position + extensionHeaderLength > reader.BaseStream.Length)
                {
                    break;
                }
                //uint8==byte  int8_t==SByte
                byte extensionID = reader.ReadByte();

                UInt32 extensionLength = reader.ReadUInt32();
                if (extensionID == 1)
                {
                    // Oct-encoded per-vertex normals
                    if (readIndex + vertexCount * 2 > reader.BaseStream.Length)
                    {
                        break;
                    }

                    meshView.octEncodedNormalBuffer = reader.ReadBytes(vertexCount * 2);
                }
                else if (enableWaterMask && extensionID == 2)
                {
                    // Water Mask
                    if (extensionLength == 1)
                    {
                        // Either fully land or fully water

                        meshView.onlyWater = reader.ReadByte() > 0;
                        meshView.onlyLand = !meshView.onlyWater;
                    }
                    else if (extensionLength == 65536)
                    {
                        // We have a 256*256 mask defining where the water is within the tile
                        // 0 means land, 255 means water
                        meshView.onlyWater = false;
                        meshView.onlyLand = false;
                        meshView.waterMaskBuffer = reader.ReadBytes(65536);
                    }
                }
                else if (extensionID == 4)
                {
                    // Metadata
                    long metaPos = reader.BaseStream.Position;
                    if (metaPos + sizeofUint32 > reader.BaseStream.Length)
                    {
                        break;
                    }
                    meshView.metadataJsonLength = reader.ReadUInt32();

                    if (metaPos + sizeofUint32 + meshView.metadataJsonLength > reader.BaseStream.Length)
                    {
                        break;
                    }

                    var metadataJson = reader.ReadBytes((int)meshView.metadataJsonLength);
                    meshView.metadataJsonBuffer = metadataJson.ReinterpretToString(System.Text.Encoding.UTF8);
                }
                else
                {
                    reader.BaseStream.Position += extensionLength;
                }
            }
            return meshView;
        }

        private bool isEOF(BinaryReader reader)
        {
            var bs = reader.BaseStream;
            return (bs.Position == bs.Length);
        }
        private double calculateSkirtHeight(Ellipsoid ellipsoid, GlobeRectangle rectangle)
        {
            double levelMaximumGeometricError = Impl.calcQuadtreeMaxGeometricError(ellipsoid) * rectangle.computeWidth();
            return levelMaximumGeometricError * 5.0;
        }

        private void decodeIndices(UInt16[] encoded, uint[] decoded)
        {
            UInt16 highest = 0;
            for (int i = 0; i < encoded.Length; ++i)
            {
                UInt16 code = encoded[i];
                UInt16 decodedIdx = (UInt16)(highest - code);
                decoded[i] = (uint)(decodedIdx);
                if (code == 0)
                {
                    ++highest;
                }
            }
        }

        private void decodeIndices(UInt16[] encoded, UInt16[] decoded)
        {
            UInt16 highest = 0;
            for (int i = 0; i < encoded.Length; ++i)
            {
                UInt16 code = encoded[i];
                UInt16 decodedIdx = (UInt16)(highest - code);
                decoded[i] = (UInt16)(decodedIdx);
                if (code == 0)
                {
                    ++highest;
                }
            }
        }

        private void decodeIndices(uint[] encoded, UInt32[] decoded)
        {
            uint highest = 0;
            for (int i = 0; i < encoded.Length; ++i)
            {
                uint code = encoded[i];
                uint decodedIdx = (uint)(highest - code);
                decoded[i] = (UInt32)(decodedIdx);
                if (code == 0)
                {
                    ++highest;
                }
            }
        }


        private byte[] generateNormals(float[] positions, uint[] indices, uint currentNumOfIndex)
        {
            byte[] normalsBuffer = new byte[positions.Length * sizeof(float)];
            var normals = normalsBuffer.ReinterpretToArray<float>();

            for (int i = 0; i < currentNumOfIndex; i += 3)
            {
                uint id0 = indices[i];
                uint id1 = indices[i + 1];
                uint id2 = indices[i + 2];
                int id0x3 = (int)(id0) * 3;
                int id1x3 = (int)(id1) * 3;
                int id2x3 = (int)(id2) * 3;

                float3 p0 = new float3(positions[id0x3], positions[id0x3 + 1], positions[id0x3 + 2]);
                float3 p1 = new float3(positions[id1x3], positions[id1x3 + 1], positions[id1x3 + 2]);
                float3 p2 = new float3(positions[id2x3], positions[id2x3 + 1], positions[id2x3 + 2]);

                float3 normal = math.cross(p1 - p0, p2 - p0);
                normals[id0x3] += normal.x;
                normals[id0x3 + 1] += normal.y;
                normals[id0x3 + 2] += normal.z;

                normals[id1x3] += normal.x;
                normals[id1x3 + 1] += normal.y;
                normals[id1x3 + 2] += normal.z;

                normals[id2x3] += normal.x;
                normals[id2x3 + 1] += normal.y;
                normals[id2x3 + 2] += normal.z;
            }

            for (int i = 0; i < normals.Length; i += 3)
            {

                float3 normal = new float3(normals[i], normals[i + 1], normals[i + 2]);
                if (!Mathd.equalsEpsilon(math.dot(normal, normal), 0.0, Mathd.EPSILON7))
                {
                    normal = math.normalize(normal);
                    normals[i] = normal.x;
                    normals[i + 1] = normal.y;
                    normals[i + 2] = normal.z;
                }
            }

            return normalsBuffer;
        }

        private byte[] generateNormals(float[] positions, UInt16[] indices, uint currentNumOfIndex)
        {
            // byte[] normalsBuffer = new byte[positions.Length * sizeof(float)];
            var normals = new float[positions.Length];// normalsBuffer.ReinterpretToArray<float>();

            for (int i = 0; i < currentNumOfIndex; i += 3)
            {
                UInt16 id0 = indices[i];
                UInt16 id1 = indices[i + 1];
                UInt16 id2 = indices[i + 2];
                int id0x3 = (int)(id0) * 3;
                int id1x3 = (int)(id1) * 3;
                int id2x3 = (int)(id2) * 3;

                float3 p0 = new float3(positions[id0x3], positions[id0x3 + 1], positions[id0x3 + 2]);
                float3 p1 = new float3(positions[id1x3], positions[id1x3 + 1], positions[id1x3 + 2]);
                float3 p2 = new float3(positions[id2x3], positions[id2x3 + 1], positions[id2x3 + 2]);

                float3 normal = math.cross(p1 - p0, p2 - p0);
                normals[id0x3] += normal.x;
                normals[id0x3 + 1] += normal.y;
                normals[id0x3 + 2] += normal.z;

                normals[id1x3] += normal.x;
                normals[id1x3 + 1] += normal.y;
                normals[id1x3 + 2] += normal.z;

                normals[id2x3] += normal.x;
                normals[id2x3 + 1] += normal.y;
                normals[id2x3 + 2] += normal.z;
            }

            for (int i = 0; i < normals.Length; i += 3)
            {

                float3 normal = new float3(normals[i], normals[i + 1], normals[i + 2]);
                if (!Mathd.equalsEpsilon(math.dot(normal, normal), 0.0, Mathd.EPSILON7))
                {
                    normal = math.normalize(normal);
                    normals[i] = normal.x;
                    normals[i + 1] = normal.y;
                    normals[i + 2] = normal.z;
                }
            }

            return normals.Reinterpret();
        }


        private void addSkirts<E, I>(Ellipsoid ellipsoid, double3 center, GlobeRectangle rectangle, double minimumHeight, double maximumHeight,
        int currentVertexCount, int currentIndicesCount, double skirtHeight, double longitudeOffset, double latitudeOffset,
         NativeArray<double3> uvsAndHeights,
         byte[] westEdgeIndicesBuffer, byte[] southEdgeIndicesBuffer,
         byte[] eastEdgeIndicesBuffer, byte[] northEdgeIndicesBuffer,
          float[] outputPositions, float[] outputNormals, I[] outputIndices)
          where E : unmanaged where I : unmanaged
        {
            int westVertexCount = (westEdgeIndicesBuffer.Length / Unsafe.SizeOf<uint>());
            int southVertexCount = (southEdgeIndicesBuffer.Length / Unsafe.SizeOf<uint>());
            int eastVertexCount = (eastEdgeIndicesBuffer.Length / Unsafe.SizeOf<uint>());
            int northVertexCount = (northEdgeIndicesBuffer.Length / Unsafe.SizeOf<uint>());

            // allocate edge indices to be sort later
            int maxEdgeVertexCount = westVertexCount;
            maxEdgeVertexCount = math.max(maxEdgeVertexCount, southVertexCount);
            maxEdgeVertexCount = math.max(maxEdgeVertexCount, eastVertexCount);
            maxEdgeVertexCount = math.max(maxEdgeVertexCount, northVertexCount);

            var westEdgeIndices = westEdgeIndicesBuffer.ReinterpretToArray<E>();
            Array.Sort(westEdgeIndices);
            addSkirt<E, I>(ellipsoid,
                     center,
                     rectangle,
                     minimumHeight,
                     maximumHeight,
                     currentVertexCount,
                     currentIndicesCount,
                     skirtHeight,
                     -longitudeOffset,
                     0.0,
                     uvsAndHeights,
                     westEdgeIndices,
                     outputPositions,
                     outputNormals,
                     outputIndices);


            currentVertexCount += westVertexCount;
            currentIndicesCount += (westVertexCount - 1) * 6;
            var southEdgeIndices = southEdgeIndicesBuffer.ReinterpretToArray<E>();
            Array.Sort(southEdgeIndices);
            addSkirt<E, I>(
           ellipsoid,
           center,
           rectangle,
           minimumHeight,
           maximumHeight,
           currentVertexCount,
           currentIndicesCount,
           skirtHeight,
           0.0,
           -latitudeOffset,
           uvsAndHeights,
           southEdgeIndices,
           outputPositions,
           outputNormals,
           outputIndices);

            currentVertexCount += southVertexCount;
            currentIndicesCount += (southVertexCount - 1) * 6;
            var eastEdgeIndices = eastEdgeIndicesBuffer.ReinterpretToArray<E>();
            Array.Sort(eastEdgeIndices);
            addSkirt<E, I>(
                ellipsoid,
                center,
                rectangle,
                minimumHeight,
                maximumHeight,
                currentVertexCount,
                currentIndicesCount,
                skirtHeight,
                longitudeOffset,
                0.0,
                uvsAndHeights,
                eastEdgeIndices,
                outputPositions,
                outputNormals,
                outputIndices);

            currentVertexCount += eastVertexCount;
            currentIndicesCount += (eastVertexCount - 1) * 6;
            var northEdgeIndices = northEdgeIndicesBuffer.ReinterpretToArray<E>();
            Array.Sort(northEdgeIndices);
            addSkirt<E, I>(
                ellipsoid,
                center,
                rectangle,
                minimumHeight,
                maximumHeight,
                currentVertexCount,
                currentIndicesCount,
                skirtHeight,
                0.0,
                latitudeOffset,
                uvsAndHeights,
                northEdgeIndices,
                outputPositions,
                outputNormals,
                outputIndices);
        }

        static void addSkirt<E, I>(Ellipsoid ellipsoid, double3 center, GlobeRectangle rectangle, double minimumHeight, double maximumHeight,
         int currentVertexCount,
            int currentIndicesCount,
            double skirtHeight,
            double longitudeOffset,
            double latitudeOffset,
             NativeArray<double3> uvsAndHeights,
            E[] edgeIndices,
            float[] positions,
            float[] normals,
            I[] indices) where E : unmanaged where I : unmanaged
        {
            double west = rectangle.getWest();
            double south = rectangle.getSouth();
            double east = rectangle.getEast();
            double north = rectangle.getNorth();

            int newEdgeIndex = currentVertexCount;
            int positionIdx = currentVertexCount * 3;
            int indexIdx = currentIndicesCount;
            for (uint i = 0; i < edgeIndices.Length; ++i)
            {
                int edgeIdx = (int)Convert.ChangeType(edgeIndices[i], typeof(int));

                double uRatio = uvsAndHeights[edgeIdx].x;
                double vRatio = uvsAndHeights[edgeIdx].y;
                double heightRatio = uvsAndHeights[edgeIdx].z;
                double longitude = math.lerp(west, east, uRatio) + longitudeOffset;
                double latitude = math.lerp(south, north, vRatio) + latitudeOffset;
                double heightMeters = math.lerp(minimumHeight, maximumHeight, heightRatio) - skirtHeight;
                double3 position = ellipsoid.cartographicToCartesian(new Cartographic(longitude, latitude, heightMeters));
                position -= center;

                positions[positionIdx] = (float)(position.x);
                positions[positionIdx + 1] = (float)(position.y);
                positions[positionIdx + 2] = (float)(position.z);

                if (normals != null)
                {
                    uint componentIndex = (uint)(3 * edgeIdx);
                    normals[positionIdx] = normals[componentIndex];
                    normals[positionIdx + 1] = normals[componentIndex + 1];
                    normals[positionIdx + 2] = normals[componentIndex + 2];
                }

                if (i < edgeIndices.Length - 1)
                {
                    int nextEdgeIdx = (int)Convert.ChangeType(edgeIndices[i + 1], typeof(int));
                    indices[indexIdx++] = (I)Convert.ChangeType(edgeIdx, typeof(I));
                    indices[indexIdx++] = (I)Convert.ChangeType(nextEdgeIdx, typeof(I));
                    indices[indexIdx++] = (I)Convert.ChangeType(newEdgeIndex, typeof(I));

                    indices[indexIdx++] = (I)Convert.ChangeType(newEdgeIndex, typeof(I));
                    indices[indexIdx++] = (I)Convert.ChangeType(nextEdgeIdx, typeof(I));
                    indices[indexIdx++] = (I)Convert.ChangeType(newEdgeIndex + 1, typeof(I));
                }

                ++newEdgeIndex;
                positionIdx += 3;
            }
        }


        struct TileRange
        {
            public uint minimumX;
            public uint minimumY;
            public uint maximumX;
            public uint maximumY;
        }

        static void processMetadata(QuadtreeTileID tileID, string metadataString, TileContentLoadResult result)
        {
            var metadata = JObject.Parse(metadataString);
            bool have_available = metadata.TryGetValue("available", out var value);
            if (!have_available || value.Type != JTokenType.Array)
            {
                return;
            }
            var available = (JArray)value;
            if (available.Count == 0)
            {
                return;
            }

            int level = tileID.level + 1;
            for (int i = 0; i < available.Count; i++)
            {
                var rangesAtLevelJson = available[i];
                if (rangesAtLevelJson.Type != JTokenType.Array)
                {
                    continue;
                }
                foreach (var rangeJson in rangesAtLevelJson.Children())
                {

                    if (rangeJson.Type != JTokenType.Object)
                    {
                        continue;
                    }
                    var qRange = new QuadtreeTileRectangularRange();
                    qRange.level = level;
                    qRange.minimumX = JsonHelpers.getUint32OrDefault(rangeJson, "startX", 0);
                    qRange.minimumY = JsonHelpers.getUint32OrDefault(rangeJson, "startY", 0);
                    qRange.maximumX = JsonHelpers.getUint32OrDefault(rangeJson, "endX", 0);
                    qRange.maximumY = JsonHelpers.getUint32OrDefault(rangeJson, "endY", 0);
                    result.availableTileRectangles.Add(qRange);
                }
                ++level;
            }

        }
    }
}